本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
中文名稱為攔截器,受到 剖面導向程式設計 (Aspect Oriented Programming) 的啟發,為原功能的擴展邏輯,其特點如下:

Interceptor 可以透過 CLI 產生:
$ nest generate interceptor <INTERCEPTOR_NAME>
注意:
<INTERCEPTOR_NAME>可以含有路徑,如:interceptors/hello-world,這樣就會在src資料夾下建立該路徑並含有 Interceptor。
這邊我建立一個 HelloWorldInterceptor 在 interceptors 資料夾下:
$ nest generate interceptor interceptors/hello-world
在 src 底下會看見一個名為 interceptors 的資料夾,裡面有 hello-world.interceptor.ts 以及 hello-world.interceptor.spec.ts:
建立出來的 Interceptor 骨架如下,會發現 Interceptor 其實也是帶有 @Injectable 裝飾器的 class,不過它必須實作 NestInterceptor 介面,並設計 intercept(context: ExecutionContext, next: CallHandler) 方法:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class HelloWorldInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle();
  }
}
CallHandler 為 Interceptor 的重要成員,它實作了 handle() 來調用路由處理的方法,進而導入對應的 Controller 方法,也就是說,如果在 Interceptor 不回傳 CallHandler 的 handle(),將會使路由處理失去運作。
由於 CallHandler 為 intercept 方法的參數,故其一定是在 intercept 中被呼叫,也就是說,可以在回傳 handle() 之前 寫一段邏輯,使其可以在進入 Controller 的方法前被執行,又因為 handle() 回傳的是 Observable,故可以透過 pipe 的方式 對回傳值做調整,使其可以在 Controller 的方法執行之後處理其他邏輯。
注意:
handle()是Observable,我們把它作為intercept的回傳值是希望 Nest 可以去subscribe它,根據Observable的特性,若沒有去subscribe它則不會執行其內部邏輯,這也是為什麼不回傳handle()的話將會使路由處理失去運作的原因。
ExecutionContext 是繼承 ArgumentsHost 的 class,其提供了更多關於此請求的相關訊息,下方為它提供的兩個方法,透過這兩個方法可以大幅提升應用的靈活性:
透過 getClass() 取得當前請求對應的 Controller Class:
const Controller: TodoController = context.getClass<TodoController>();
透過 getHandler() 取得當前請求對應的 Controller method,假設當前請求會呼叫 TodoController 的 getAll(),那就會回傳 getAll 這個函式:
const method: Function = context.getHandler();
在使用之前,先將 hello-world.interceptor.ts 修改一下,在進入 Interceptor 時印出 Hello World! 並使用變數儲存進入的時間,再透過 tap 印出結束的時間與進入的時間差:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class HelloWorldInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Hello World!');
    const input = Date.now();
    const handler = next.handle();
    return handler.pipe(
      tap(() => console.log(`${ Date.now() - input } ms`))
    );
  }
}
修改完以後就來使用此 Interceptor,透過 @UseInterceptors 裝飾器即可輕鬆套用,使用的方式大致上可以分成兩種:
@UseInterceptors 裝飾器,只會針對該資源套用。@UseInterceptors 裝飾器,會針對整個 Controller 中的資源套用。下方以套用在 Controller 為例,修改 app.controller.ts:
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';
@Controller()
@UseInterceptors(HelloWorldInterceptor)
export class AppController {
  constructor(private readonly appService: AppService) {}
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
透過瀏覽器查看 http://localhost:3000 會發現終端機出現了下方結果:
Hello World!
3 ms
如果設計了一個共用的 Interceptor 要套用在所有資源上的話,只需要修改 main.ts 即可,透過 useGlobalInterceptors 來配置全域 Interceptor:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new HelloWorldInterceptor());
  await app.listen(3000);
}
bootstrap();
上面的方法是透過模組外部完成全域配置的,與 Pipe 一樣可以用依賴注入的方式,透過指定 Provider 的 token 為 APP_INTERCEPTOR 來實現,這裡是用 useClass 來指定要建立實例的類別:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';
@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useClass: HelloWorldInterceptor
    }
  ]
})
export class AppModule {}
Interceptor 可以在不修改 Controller 的情況下去擴充邏輯,是十分方便的功能。這裡附上今天的懶人包:
CallHandler 為重要成員,需要呼叫其 handle() 來讓路由機制得以運行。ExecutionContext 提供了 getClass() 與 getHandler() 來提升靈活性。CallHandler 為 Interceptor 的重要成員,它實作了 handle() 來調用路由處理的方法,進而導入對應的 Controller 方法,也就是說,如果在 Interceptor 不調用 CallHandler 的 handle(),將會使路由處理失去運作。
您好,根据文中示例,尝试了一下intercept返回不是CallHandler.handle()及其rxjs/operators处理结果的情况,发现也會使路由處理失去運作。
如果在 Interceptor 不調用 CallHandler 的 handle(),將會使路由處理失去運作。
改为"如果在 Interceptor 不調用 CallHandler 的 handle()并将其结果返回,將會使路由處理失去運作。"是不是更精确些呢
你好,不好意思,我說明的不夠精確,handle() 基本上就是呼叫 Controller 對應的 handler,並以 Observable 的形式讓我們可以用 RxJS 的技巧來針對 handler 處理完的資料做進一步的處理,如果返回值不是 handle() 所串聯的事件流,那 NestJS 就無法 subscribe 到它,而 Observable 沒有被 subscribe 的情況下是不會被執行的,就會造成所謂「路由處理失去運作」的情況。
我有更新文章進行補充了,感謝你的建議!
看到中間突然冒出個RxJS的東西卡住了好久不懂,去官往翻了一下還是不懂observable的概念...
https://rxjs-dev.firebaseapp.com/guide/observable
你好,Observable 是一種可觀察的物件,是 RxJS 裡面的核心角色之一,透過訂閱的方式來觀察這個物件提供的資料。
詳細的內容可以參考 Mike 大大的文章,基本上 Observable 是觀察者模式(Observable Pattern)的實作,當然,在 RxJS 的世界裡,有很多種可觀察的物件,在這篇文章中有非常詳細的說明。
謝謝您 我了解一下
感謝您直接提供參考文章,比起在網路上搜索大海撈針真的省很多時間